/*
* Copyright (C) 2010-2012 Magnus Skjegstad
* 
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
* 
* The latest version of this library can be downloaded from http://mist-pubsub.googlecode.com
* 
*/
package com.skjegstad.mist2.datatypes;

import com.skjegstad.mist2.hash.MurmurHash;
import java.util.Collection;
import java.util.LinkedList;

/**
 *
 * @author Magnus Skjegstad
 */
public class TopicIdentifier {
    private LinkedList<Integer> hashes; // order matters
    private final int anySingleKeywords = MurmurHash.hash32("?");
    private final int anyKeywords = MurmurHash.hash32("*");

    /**
     * Create a topic hash from an array of strings. The strings are
     * typically the sections of a namespace. E.g. com.skjegstad.test becomes a list
     * with the items "com", "skjegstad" and "test".
     *
     * @param topicStrings array of strings to include in topic hash.
     */
    public TopicIdentifier(String[] topicStrings) {
        hashes = new LinkedList();

        for (String s : topicStrings) {
            hashes.add(MurmurHash.hash32(s));
        }
    }

    /**
     * Create a topic hash from a namespace string. Each section of the namespace is hashed
     * and added to the topic. E.g. com.skjegstad.test is split into "com", "skjegstad" and "test". Each
     * string is then stored in the topic as a 64-bit hash value.
     */
    public TopicIdentifier(String namespace) {
        this(namespace.trim().split("\\."));        
    }

    public LinkedList<Integer> getHashes() {
        return hashes;
    }

    public TopicIdentifier(LinkedList<Integer> hashes) {
        this.hashes = hashes;
    }

    /**
     * Check if this topic has a partial prefix match for prefixTopic. E.g. if
     * the prefix topic is "com.skjegstad" and this topic is "com.skjegstad.test",
     * the method returns true because "com.skjegstad" is a valid prefix of "com.skjegstad.test".
     * However, "com.skjeg" would return false, as only whole sections are matched
     * in the namespace. Similarly, "skjegstad" would also return false, as the "com"-prefix
     * is missing.
     *
     * @param prefixTopic
     * @return true if this topic starts with partialTopic
     */
    public boolean prefixMatch(TopicIdentifier prefixTopic) {
        LinkedList<Integer> partialHashes = prefixTopic.getHashes();
        if (partialHashes.size() > this.hashes.size()) // partial topic can not be longer than this topic
            return false;
        for (int i = 0; i < partialHashes.size(); i++)
            if (partialHashes.get(i).intValue() != this.hashes.get(i).intValue())
                return false; // no match. All hashes must be equal.

        return true; // if we got here, the partialTopic matches our topic
    }

    /**
     * Check if this topic has an exact match with the given topic. 
     * 
     * @param topic topic to compare to.
     * @return true if topics are identical.
     */
    public boolean matchExact(TopicIdentifier topic) {
        return this.equals(topic);
    }

    /**
     * Check if one (or more) of the provided topics are identical to our topic.
     * 
     * @param topics list of topics to compare to
     * @return true if one or more of the topics are identical to our topic.
     */
    public boolean matchExactAny(Collection<TopicIdentifier> topics) {
        return topics.contains(this);
    }
    
    /**
     * Matches topic on wildcards. The available wildcards are ? and *, where ? matches a single keyword and * matches any keywords.
     * Limitations: 
     * * may only be used at the end of topics, e.g. no.ffi.*.
     * When ? is used at the end of a namespace it also matches empty strings. E.g. no.ffi.? matches no.ffi.
     * @param topicWithWildcard
     * @return 
     */
    public boolean matchWildcard(TopicIdentifier topicWithWildcard) {
        int i = 0;
        for (i = 0; i < topicWithWildcard.getHashes().size(); i++) {
            // if its a wildcard match on any keywords, break the loop and return true
            if (topicWithWildcard.getHashes().get(i).intValue() == anyKeywords) 
                return true;
            
            // if its a wildcard match on a single keyword, continue
            if (topicWithWildcard.getHashes().get(i).intValue() == anySingleKeywords) 
                continue;
        
            // otherwise, if we are beyond the length of the hashes we are comparing to, return false
           if (i >= this.hashes.size())
               return false;
            
           // Finally, compare the actual hash in this position
            if (topicWithWildcard.getHashes().get(i).intValue() != this.hashes.get(i).intValue())
               return false; // no match.
        }

        return i == this.getHashes().size(); // if we got here, the partialTopic matches our topic
    }
    
    public boolean matchWildcardAny(Collection<TopicIdentifier> topics) {
        for (TopicIdentifier t : topics)
            if (matchWildcard(t))
                return true;
        return false;
    }
    

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final TopicIdentifier other = (TopicIdentifier) obj;
        if (this.hashes != other.hashes && (this.hashes == null || !this.hashes.equals(other.hashes))) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 47 * hash + (this.hashes != null ? this.hashes.hashCode() : 0);
        return hash;
    }

    @Override
    public String toString() {
        return "{" + hashes + "}";
    }    
    
    
}
